﻿using Microsoft.Owin.Security;
using Sustainalytics.LongRunningTaskManager.Interface;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Sustainalytics.LongRunningTaskManager
{
    /// <summary>
    /// Instance of a Long Running Task as seen by the Task Manager.
    /// </summary>
    public class LongRunningTaskInstance : ILongRunningTaskInstance, IEqualityComparer<LongRunningTaskInstance>
    {
        /// <summary>
        /// Task instance ID.
        /// </summary>
        public string Id { get; private set; }

        /// <summary>
        /// Task instance title.
        /// </summary>
        public string Title { get; private set; }

        /// <summary>
        /// Task associated to current task instance.
        /// </summary>
        private ILongRunningTask Task { get; set; }

        /// <summary>
        /// Task context.
        /// </summary>
        public LongRunningTaskContext TaskContext { get; private set; }

        /// <summary>
        /// Managed task associated to current task instance.
        /// </summary>
        public Task<ILongRunningTaskResult> ManagedTask { get; private set; }

        /// <summary>
        /// Result of current task instance.
        /// </summary>
        public ILongRunningTaskResult Result { get; private set; }

        /// <summary>
        /// Status of current task instance.
        /// </summary>
        public LongRunningTaskInstanceStatus Status { get; private set; }

        /// <summary>
        /// Task type.
        /// </summary>
        public LongRunningTaskTypeEnum Type { get { return this.Task.Type; } }

        /// <summary>
        /// Date when current task instance was created.
        /// </summary>
        public DateTime DateCreated { get; private set; }

        /// <summary>
        /// Order of occurence.
        /// </summary>
        public long Order { get; private set; }

        /// <summary>
        /// Task result processing stages.
        /// </summary>
        public ILongRunningTaskResultTransformer TaskResultProcess { get; private set; }

        /// <summary>
        /// Async info.
        /// </summary>
        public ILongRunningTaskInstanceAsyncInfo AsyncInfo { get; private set; }

        /// <summary>
        /// Task cancellation token source used to cancel current task.
        /// Considers also the Task Manager cancellation token.
        /// </summary>
        private CancellationTokenSource TaskCancellationTokenSource { get; set; }


        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="taskFactory"> Task factory used to create tasks. </param>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <param name="id"> Task instance ID. </param>
        /// <param name="title"> Task instance title. </param>
        /// <param name="paramJson"> Task parameters. </param>
        /// <param name="taskType"> Task type. </param>
        /// <param name="taskResultTransformer"> Transformer used to process task results. </param>
        /// <param name="parentCancellationToken"> Parent cancellation token which controls cancellation of all tasks. </param>
        /// <param name="sem"> Semaphore used to control task instances creation. </param>
        public LongRunningTaskInstance(
            ILongRunningTaskFactory taskFactory,
            AuthenticationTicket ticket,
            string id,
            string title,
            string paramJson,
            LongRunningTaskTypeEnum taskType,            
            ILongRunningTaskResultTransformer taskResultTransformer,
            CancellationToken parentCancellationToken,
            long? order,
            bool isUpload,
            bool isDownload)
        {
            // create task
            ILongRunningTask task = CreateTask(taskType, taskFactory);

            // create task cancellation token source
            this.TaskCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(parentCancellationToken);

            // set properties
            this.Id = id;
            this.Title = title;
            this.Task = task;
            this.DateCreated = DateTime.UtcNow;
            this.Order = order ?? this.DateCreated.Ticks;
            this.TaskResultProcess = taskResultTransformer;


            // create status
            this.Status = new LongRunningTaskInstanceStatus(
                id: id,
                title: title,
                type: this.Type,
                taskInstanceStatus: LongRunningTaskInstanceStatusEnum.Scheduled,
                taskStatus: new LongRunningTaskStatus(),
                order: this.Order,
                isUpload: isUpload,
                isDownload: isDownload);

            // create task context
            this.TaskContext = new LongRunningTaskContext(
                taskInstance: this,
                ticket: ticket,
                paramJson: paramJson,
                cancellationToken: this.TaskCancellationTokenSource.Token);

            // create managed task associated with current instance
            this.ManagedTask = CreateManagedTask();

            // create async info
            this.AsyncInfo = new LongRunningTaskInstanceAsyncInfo(this.ManagedTask, this.TaskCancellationTokenSource.Token);
            ((LongRunningTaskInstanceAsyncInfo)this.AsyncInfo).SetTaskStatus(LongRunningTaskInstanceStatusEnum.Scheduled);
        }

        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="taskFactory"> Task factory used to create tasks. </param>
        /// <param name="taskInstanceSnapshot"> Task instance snapshot. </param>
        /// <param name="ticket"> Auhentication ticket. </param>
        /// <param name="taskResultTransformer"> Transformer used to process tasks results. </param>
        /// <param name="parentCancellationToken"> Parent cancellation token used to control cancellation for all tasks. </param>
        /// <param name="sem"> Semaphore used to control task instances creation. </param>
        public LongRunningTaskInstance(
            ILongRunningTaskFactory taskFactory,
            LongRunningTaskInstanceSnapshot taskInstanceSnapshot,
            AuthenticationTicket ticket,
            ILongRunningTaskResultTransformer taskResultTransformer,
            CancellationToken parentCancellationToken)
        {
            // create task
            ILongRunningTask task = taskFactory.CreateLongRunningTask(taskInstanceSnapshot.Type);

            // create task cancellation token source
            this.TaskCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(parentCancellationToken);

            // set properties
            this.Id = taskInstanceSnapshot.Id;
            this.Title = taskInstanceSnapshot.Title;
            this.Task = task;
            this.DateCreated = taskInstanceSnapshot.DateCreated;
            this.Order = taskInstanceSnapshot.Order;
            this.Result = taskInstanceSnapshot.Result;

            // create status
            this.Status = new LongRunningTaskInstanceStatus(
                id: taskInstanceSnapshot.Id,
                title: taskInstanceSnapshot.Title,
                type: this.Type,
                taskInstanceStatus: taskInstanceSnapshot.Status.TaskInstanceStatus,
                taskStatus: taskInstanceSnapshot.Status.TaskStatus,
                order: this.Order,
                isUpload: taskInstanceSnapshot.Status.IsUpload,
                isDownload: taskInstanceSnapshot.Status.IsDownload);

            // create task context
            this.TaskContext = new LongRunningTaskContext(
                taskInstance: this,
                ticket: ticket,
                paramJson: taskInstanceSnapshot.ParamJson,
                cancellationToken: this.TaskCancellationTokenSource.Token);

            // create managed task associated with current instance
            this.ManagedTask = System.Threading.Tasks.Task.FromResult(taskInstanceSnapshot.Result);

            // create async info
            this.AsyncInfo = new LongRunningTaskInstanceAsyncInfo(this.ManagedTask, this.TaskCancellationTokenSource.Token);
            ((LongRunningTaskInstanceAsyncInfo)this.AsyncInfo).SetTaskStatus(taskInstanceSnapshot.Status.TaskInstanceStatus);
        }

        /// <summary>
        /// Create task.
        /// </summary>
        /// <param name="taskFactory"> Task factory. </param>
        /// <returns> Task. </returns>
        private ILongRunningTask CreateTask(LongRunningTaskTypeEnum taskType, ILongRunningTaskFactory taskFactory)
        {
            // create task
            ILongRunningTask task = taskFactory.CreateLongRunningTask(taskType);

            if (task == null)
            {
                throw new LongRunningTaskTypeNotSupportedException(taskType);
            }

            return task;
        }

        /// <summary>
        /// Create managed task associated with current instance
        /// </summary>
        private Task<ILongRunningTaskResult> CreateManagedTask()
        {
            // create managed task
            Task<ILongRunningTaskResult> task = new Task<ILongRunningTaskResult>(() =>
                {
                    try
                    {
                        // check for cancellations
                        this.TaskCancellationTokenSource.Token.ThrowIfCancellationRequested();

                        // set as running
                        SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum.Running);
                        LongRunningTaskManagerEventSource.Log.TaskRunning(this.Task.Type, this.Id, this.DateCreated.ToString(CultureInfo.InvariantCulture));

                        // start task by passing the context
                        object taskResultObj = this.Task.Start(this.TaskContext);

                        // create result
                        ILongRunningTaskResult taskResult = CreateResult(taskResultObj);

                        // set result
                        this.Result = taskResult;

                        if (!this.Status.TaskStatus.Errors.Any())
                        {
                            // set as finished
                            SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum.Finished);
                            LongRunningTaskManagerEventSource.Log.TaskFinished(this.Task.Type, this.Id, this.DateCreated.ToString(CultureInfo.InvariantCulture));
                        }
                        else
                        {
                            // set as failed
                            SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum.Failed);
                            LongRunningTaskManagerEventSource.Log.TaskFailed(this.Task.Type, this.Id, this.DateCreated.ToString(CultureInfo.InvariantCulture), "Task reported errors.");
                        }

                        return taskResult;
                    }
                    catch (OperationCanceledException)
                    {
                        // set as cancelled
                        SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum.Cancelled);
                        LongRunningTaskManagerEventSource.Log.TaskCancelled(this.Task.Type, this.Id, this.DateCreated.ToString(CultureInfo.InvariantCulture));
                    }
                    catch (AggregateException ex)
                    {
                        bool isCancellation = ex.InnerExceptions.All(e => typeof(OperationCanceledException).IsAssignableFrom(e.GetType()));

                        // check if task was cancelled - OperationCancelledException is part of AggregateException
                        if (isCancellation)
                        {
                            // set as cancelled
                            SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum.Cancelled);
                            LongRunningTaskManagerEventSource.Log.TaskCancelled(this.Task.Type, this.Id, this.DateCreated.ToString(CultureInfo.InvariantCulture));
                        }
                        else
                        {
                            // set as failed
                            SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum.Failed);
                            LongRunningTaskManagerEventSource.Log.TaskFailed(this.Task.Type, this.Id, this.DateCreated.ToString(CultureInfo.InvariantCulture), ex);
                        }
                    }
                    catch (Exception ex)
                    {
                        // set as failed
                        SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum.Failed);
                        LongRunningTaskManagerEventSource.Log.TaskFailed(this.Task.Type, this.Id, this.DateCreated.ToString(CultureInfo.InvariantCulture), ex);
                    }

                    return null;
                },
                // task will not be started if cancellation was requested
                this.TaskCancellationTokenSource.Token);

                // task continuation when cancellation was found
                task.ContinueWith(prevTask =>
                {
                    // set task as cancelled
                    SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum.Cancelled);
                }, 
                TaskContinuationOptions.OnlyOnCanceled);

            return task;
        }

        private void SetTaskInstanceStatus(LongRunningTaskInstanceStatusEnum taskStatus)
        {
            // set task instance status
            this.Status.TaskInstanceStatus = taskStatus;

            // set async info state
            ((LongRunningTaskInstanceAsyncInfo)this.AsyncInfo).SetTaskStatus(taskStatus);
        }

        /// <summary>
        /// Create task result from object.
        /// </summary>
        /// <param name="result"> Task result as an object. </param>
        /// <returns> Task result. </returns>
        private ILongRunningTaskResult CreateResult(object taskResultObj)
        {
            ILongRunningTaskResult taskResult;

            if (taskResultObj is ILongRunningTaskResult)
            {
                // result is a typed task result
                taskResult = taskResultObj as ILongRunningTaskResult;
            }
            else
            {
                // result is an object
                taskResult = new LongRunningTaskObjectResult { Data = taskResultObj };
            }

            // pass result to the processing stage if available
            if(this.TaskResultProcess != null)
            {
                taskResult = this.TaskResultProcess.Process(taskResult, this.TaskContext);
            }

            return taskResult;
        }

        /// <summary>
        /// Start task.
        /// </summary>
        public void Start()
        {
            // start task
            this.ManagedTask.Start();
        }

        /// <summary>
        /// Cancel current task instance.
        /// </summary>
        public void Cancel()
        {
            TaskCancellationTokenSource.Cancel();
        }


        public bool Equals(LongRunningTaskInstance x, LongRunningTaskInstance y)
        {
            if (x == null && y == null)
                return true;

            if (x == null || y == null)
                return false;

            return ((x.Id == null && y.Id == null) || x.Id == y.Id)
                && ((x.Title == null && y.Title == null) || x.Title == y.Title)
                && ((x.Task == null && y.Task == null) || x.Task == y.Task)
                && ((x.TaskContext == null && y.TaskContext == null) || x.TaskContext == y.TaskContext)
                && ((x.ManagedTask == null && y.ManagedTask == null) || x.ManagedTask == y.ManagedTask)
                && ((x.Result == null && y.Result == null) || x.Result == y.Result)
                && ((x.Status == null && y.Status == null) || x.Status == y.Status)
                && (x.DateCreated.Date == y.DateCreated.Date)
                && (x.Order == y.Order)
                && x.Type == y.Type;
        }

        public int GetHashCode(LongRunningTaskInstance obj)
        {
            return obj.Id.GetHashCode();
        }
    }
}
